/*
 * Created on 13-Dec-2004
 * Created by Paul Gardner
 * Copyright (C) Azureus Software, Inc, All Rights Reserved.
 *
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU General Public License
 * as published by the Free Software Foundation; either version 2
 * of the License, or (at your option) any later version.
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
 *
 */

package com.aelitis.azureus.core.proxy.socks.impl;

import java.io.IOException;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.nio.channels.SocketChannel;

import org.gudy.azureus2.core3.util.Debug;
import org.gudy.azureus2.core3.util.DirectByteBuffer;
import org.gudy.azureus2.core3.util.DirectByteBufferPool;
import org.gudy.azureus2.core3.util.HostNameToIPResolver;

import com.aelitis.azureus.core.networkmanager.admin.NetworkAdmin;
import com.aelitis.azureus.core.proxy.AEProxyConnection;
import com.aelitis.azureus.core.proxy.socks.AESocksProxy;
import com.aelitis.azureus.core.proxy.socks.AESocksProxyAddress;
import com.aelitis.azureus.core.proxy.socks.AESocksProxyConnection;
import com.aelitis.azureus.core.proxy.socks.AESocksProxyPlugableConnection;

/**
 * @author parg
 * 
 */

public class AESocksProxyPlugableConnectionDefault implements AESocksProxyPlugableConnection {
    protected AESocksProxyConnection socks_connection;
    protected AEProxyConnection connection;

    protected SocketChannel source_channel;
    protected SocketChannel target_channel;

    protected proxyStateRelayData relay_data_state;

    public AESocksProxyPlugableConnectionDefault(AESocksProxyConnection _socks_connection) {
        socks_connection = _socks_connection;
        connection = socks_connection.getConnection();

        source_channel = connection.getSourceChannel();
    }

    public String getName() {
        if (target_channel != null) {

            return (target_channel.socket().getInetAddress() + ":" + target_channel.socket().getPort());
        }

        return ("");
    }

    public InetAddress getLocalAddress() {
        return (target_channel.socket().getInetAddress());
    }

    public int getLocalPort() {
        return (target_channel.socket().getPort());
    }

    public void connect(AESocksProxyAddress _address)

    throws IOException {
        InetAddress address = _address.getAddress();

        if (address == null) {

            if (socks_connection.areDNSLookupsEnabled()) {

                try {
                    address = HostNameToIPResolver.syncResolve(_address.getUnresolvedAddress());

                } catch (Throwable e) {
                }
            }

            if (address == null) {

                throw (new IOException("DNS lookup of '" + _address.getUnresolvedAddress() + "' fails"));
            }
        }

        new proxyStateRelayConnect(new InetSocketAddress(address, _address.getPort()));
    }

    public void relayData()

    throws IOException {
        new proxyStateRelayData();
    }

    public void close() {
        if (target_channel != null) {

            try {
                connection.cancelReadSelect(target_channel);
                connection.cancelWriteSelect(target_channel);

                target_channel.close();

            } catch (Throwable e) {

                Debug.printStackTrace(e);
            }
        }

        if (relay_data_state != null) {

            relay_data_state.destroy();
        }
    }

    protected class proxyStateRelayConnect extends AESocksProxyState {
        protected InetSocketAddress address;

        protected proxyStateRelayConnect(InetSocketAddress _address)

        throws IOException {
            super(socks_connection);

            address = _address;

            // OK, we're almost ready to roll. Unregister the read select until we're
            // connected

            connection.cancelReadSelect(source_channel);

            connection.setConnectState(this);

            target_channel = SocketChannel.open();

            InetAddress bindIP = NetworkAdmin.getSingleton().getMultiHomedOutgoingRoundRobinBindAddress(address.getAddress());

            if (bindIP != null) {

                try {
                    target_channel.socket().bind(new InetSocketAddress(bindIP, 0));

                } catch (IOException e) {

                    if (bindIP.isAnyLocalAddress()) {

                        // no point in moaning here about this

                    } else {

                        // if the address is unresolved then the calculated bind address can be invalid
                        // (might pick an IPv6 address for example when this is unbindable). In this case
                        // carry on and attempt to connect as this will fail anyway

                        if (!(e.getMessage().contains("not supported") && address.isUnresolved())) {

                            throw (e);
                        }
                    }
                }
            }

            target_channel.configureBlocking(false);

            target_channel.connect(address);

            connection.requestConnectSelect(target_channel);
        }

        protected boolean connectSupport(SocketChannel sc)

        throws IOException {
            if (!sc.finishConnect()) {

                throw (new IOException("finishConnect returned false"));
            }

            // if we've got a proxy chain, now's the time to negotiate the connection

            AESocksProxy proxy = socks_connection.getProxy();

            if (proxy.getNextSOCKSProxyHost() != null) {

            }

            socks_connection.connected();

            return (true);
        }
    }

    protected class proxyStateRelayData extends AESocksProxyState {
        protected DirectByteBuffer source_buffer;
        protected DirectByteBuffer target_buffer;

        protected long outward_bytes = 0;
        protected long inward_bytes = 0;

        protected proxyStateRelayData()

        throws IOException {
            super(socks_connection);

            source_buffer = DirectByteBufferPool.getBuffer(DirectByteBuffer.AL_PROXY_RELAY, 1024);
            target_buffer = DirectByteBufferPool.getBuffer(DirectByteBuffer.AL_PROXY_RELAY, 1024);

            relay_data_state = this;

            if (connection.isClosed()) {

                destroy();

                throw (new IOException("connection closed"));
            }

            connection.setReadState(this);

            connection.setWriteState(this);

            connection.requestReadSelect(source_channel);

            connection.requestReadSelect(target_channel);

            connection.setConnected();
        }

        protected void destroy() {
            if (source_buffer != null) {

                source_buffer.returnToPool();

                source_buffer = null;
            }

            if (target_buffer != null) {

                target_buffer.returnToPool();

                target_buffer = null;
            }
        }

        protected boolean readSupport(SocketChannel sc)

        throws IOException {
            connection.setTimeStamp();

            SocketChannel chan1 = sc;
            SocketChannel chan2 = sc == source_channel ? target_channel : source_channel;

            DirectByteBuffer read_buffer = sc == source_channel ? source_buffer : target_buffer;

            int len = read_buffer.read(DirectByteBuffer.SS_PROXY, chan1);

            if (len == -1) {

                // means that the channel has been shutdown

                connection.close();

            } else {

                if (read_buffer.position(DirectByteBuffer.SS_PROXY) > 0) {

                    read_buffer.flip(DirectByteBuffer.SS_PROXY);

                    int written = read_buffer.write(DirectByteBuffer.SS_PROXY, chan2);

                    if (chan1 == source_channel) {

                        outward_bytes += written;

                    } else {

                        inward_bytes += written;
                    }

                    if (read_buffer.hasRemaining(DirectByteBuffer.SS_PROXY)) {

                        connection.cancelReadSelect(chan1);

                        connection.requestWriteSelect(chan2);

                    } else {

                        read_buffer.position(DirectByteBuffer.SS_PROXY, 0);

                        read_buffer.limit(DirectByteBuffer.SS_PROXY, read_buffer.capacity(DirectByteBuffer.SS_PROXY));
                    }
                }
            }

            return (len > 0);
        }

        protected boolean writeSupport(SocketChannel sc)

        throws IOException {
            // socket SX -> SY via BX
            // so if SX = source_channel then BX is target buffer

            SocketChannel chan1 = sc;
            SocketChannel chan2 = sc == source_channel ? target_channel : source_channel;

            DirectByteBuffer read_buffer = sc == source_channel ? target_buffer : source_buffer;

            int written = read_buffer.write(DirectByteBuffer.SS_PROXY, chan1);

            if (chan1 == target_channel) {

                outward_bytes += written;

            } else {

                inward_bytes += written;
            }

            if (read_buffer.hasRemaining(DirectByteBuffer.SS_PROXY)) {

                connection.requestWriteSelect(chan1);

            } else {

                read_buffer.position(DirectByteBuffer.SS_PROXY, 0);

                read_buffer.limit(DirectByteBuffer.SS_PROXY, read_buffer.capacity(DirectByteBuffer.SS_PROXY));

                connection.requestReadSelect(chan2);
            }

            return (written > 0);
        }

        public String getStateName() {
            String state = this.getClass().getName();

            int pos = state.indexOf("$");

            state = state.substring(pos + 1);

            return (state + " [out=" + outward_bytes + ",in=" + inward_bytes + "] " + source_buffer + " / " + target_buffer);
        }
    }
}
